package flight.services.amf
{
	import flash.utils.ByteArray;
	import flash.xml.XMLDocument;
	
	public class AMF3Parser 
	{
		public static const AMF3_UNDEFINED:uint				= 0x00;
	    public static const AMF3_NULL:uint              	= 0x01;
	    public static const AMF3_BOOLEAN_FALSE:uint     	= 0x02;
	    public static const AMF3_BOOLEAN_TRUE:uint      	= 0x03;
	    public static const AMF3_INTEGER:uint           	= 0x04;
	    public static const AMF3_NUMBER:uint            	= 0x05;
	    public static const AMF3_STRING:uint            	= 0x06;
	    public static const AMF3_XML:uint               	= 0x07;					
	    public static const AMF3_DATE:uint              	= 0x08;
	    public static const AMF3_ARRAY:uint             	= 0x09;
	    public static const AMF3_OBJECT:uint            	= 0x0A;
	    public static const AMF3_XMLSTRING:uint         	= 0x0B;
	    public static const AMF3_BYTEARRAY:uint         	= 0x0C;
	    
	    // These AMF3 types may be referenced by object (as per AMF3 spec)
		// AMF3_DATE, AMF3_ARRAY, AMF3_OBJECT
		protected var referencedObjects:Array = new Array();
		
    	// These AMF3 types may be referenced by string (as per AMF3 spec)
		// AMF3_STRING
		protected var referencedStrings:Array = new Array();
	
    	// These AMF3 types may be referenced by class definition (as per AMF3 spec)
		// AMF3_OBJECT (internal classes and objects)
		protected var referencedClassDefs:Array = new Array();
		
		public function clearAllReferences():void
		{
			referencedObjects = new Array();
			referencedStrings = new Array();
			referencedClassDefs = new Array();
		}
		
		public function determineTypeMarker(data:*):uint
		{
			var typeMarker:uint = AMF3_OBJECT;
			
			if (data === null) {
				typeMarker = AMF3_NULL;
			} else if (data === undefined) {
				typeMarker = AMF3_UNDEFINED;
			} else  if (data is int || data is uint) {
				var num:Number = (data as Number);
				if (num > 0xFFFFFFF || num < -268435456) {
					typeMarker = AMF3_NUMBER;
				} else {
					typeMarker = AMF3_INTEGER;
				} 
			} else if (data is Number) {
			   	typeMarker = AMF3_NUMBER;
			} else if (data is Boolean) {
				typeMarker = (data as Boolean)? AMF3_BOOLEAN_TRUE: AMF3_BOOLEAN_FALSE;
			} else if (data is String) {
				typeMarker = AMF3_STRING;
			} else if (data is Array) {
				typeMarker = AMF3_ARRAY;
			} else if (data is Date) {
				typeMarker = AMF3_DATE;
			} else if (data is ByteArray) {
				typeMarker = AMF3_BYTEARRAY;
			} else if (data is XML || data is XMLDocument) {
				typeMarker = AMF3_XMLSTRING;			
			} else if (data is Object) {
				typeMarker = AMF3_OBJECT;
			}
			
			return typeMarker;
		}
	
		public function readTypeMarkerDataObject(src:ByteArray):Object
		{
			var data:Object = null;
			
			try {
				var typeMarker:uint = src.readByte();
			
				switch (typeMarker) {
					case AMF3_UNDEFINED:
						data = undefined;
						break;
						
					case AMF3_NULL:
						data = null;
						break;
						          
					case AMF3_BOOLEAN_FALSE:
						data = false;
						break;
						
					case AMF3_BOOLEAN_TRUE:
						data = true;
						break;
					      	
					case AMF3_INTEGER:
						data = readVariableLengthInt(src);
						break;
						           	
					case AMF3_NUMBER:
						data = src.readDouble();
						break;
						            	
					case AMF3_STRING:
						data = readString(src);
						break;
						            	
					case AMF3_DATE:
						data = readDate(src);
						break;

					case AMF3_ARRAY:
						data = readArray(src);
						break;
						             	
					case AMF3_OBJECT:   
						data = readObject(src);
						break;
						         	
					case AMF3_XML:	
					case AMF3_XMLSTRING:
						data = readXmlString(src);
						break;
						         	
					case AMF3_BYTEARRAY:
						data = readByteArray(src);
						break;
				}					
			} catch (err:Error) {
				trace("AMFMessage parsing error: " + err.toString());
			}
			
			return data;
		}
		
		public function writeTypeMarkerDataObject(dst:ByteArray, data:Object):void
		{
			var typeMarker:uint = determineTypeMarker(data);
			dst.writeByte(typeMarker);
			
			switch (typeMarker) {
				case AMF3_UNDEFINED:
				case AMF3_NULL:          
				case AMF3_BOOLEAN_FALSE:
				case AMF3_BOOLEAN_TRUE:
					break;
				      	
				case AMF3_INTEGER:
					writeVariableLengthInt(dst, data as uint);
					break;
					           	
				case AMF3_NUMBER:
					dst.writeDouble(data as Number);
					break;
					            	
				case AMF3_STRING:
					writeString(dst, data as String);
					break;
					            	
				case AMF3_DATE:
					writeDate(dst, data as Date);
					break;

				case AMF3_ARRAY:
					writeArray(dst, data as Array);
					break;
					             	
				case AMF3_OBJECT:   
					writeObject(dst, data);
					break;
					         	
				case AMF3_XML:     
				case AMF3_XMLSTRING:
					writeXmlString(dst, data);
					break;
					         	
				case AMF3_BYTEARRAY:
					writeByteArray(dst, data as ByteArray);
					break;
			}
		}
		
		protected function _writeTypeReference(referenceTypeArray:Array, dst:ByteArray, value:Object):Boolean
		{
			var refIdx:int = referenceTypeArray.indexOf(value); 
			if (refIdx >= 0) {
				var traitsInfo:uint = (refIdx << 1); 
				writeVariableLengthInt(dst, traitsInfo);
				return true;
			} 
			
			referenceTypeArray.push(value);			
			return false;
		}
		
		protected function writeObjectReference(dst:ByteArray, value:Object):Boolean
		{
			return _writeTypeReference(referencedObjects, dst, value);
		}
		
		protected function writeStringReference(dst:ByteArray, value:String):Boolean
		{
			return _writeTypeReference(referencedStrings, dst, value);
		}
		
		protected function writeClassDefReference(dst:ByteArray, value:Object):Boolean
		{
			return _writeTypeReference(referencedClassDefs, dst, value);
		}
		
		protected function _readReferencedTypeObject(referenceTypeArray:Array, src:ByteArray):Object
		{
			var value:Object;
			var srcPos:uint = src.position;
			var traitsInfo:uint = readVariableLengthInt(src);
			var isRef:Boolean = ((traitsInfo & 0x01) == 0);
			
			if (isRef) {
				var refIdx:uint = (traitsInfo >> 1);
				if (refIdx < referenceTypeArray.length) {
					value = referenceTypeArray[refIdx];
				}
			} else {
				// this isn't an object that we've referenced before.
				// back up and let flash grab the new value for us.
				src.position = srcPos - 1;
				value = src.readObject();
				
				// keep the object around, in case we need to use de-reference it later.
				referenceTypeArray.push(value);
			}
			
			return value;
		}
		
		protected function readObject(src:ByteArray):Object
		{
			return _readReferencedTypeObject(referencedObjects, src);
		}
		
		protected function writeObject(dst:ByteArray, value:Object):void
		{
			if (!writeObjectReference(dst, value)) {
				dst.position--;
				dst.writeObject(value);
			}
		}
		
		protected function readVariableLengthInt(src:ByteArray):uint
		{
			const maxBytes:uint = 4;
			
			var value:uint = 0;
			var bytesRead:uint = 0;			
			var byteRef:uint = 0;
			
			try {
				byteRef = src.readByte();
				while (((byteRef & 0x80) != 0) && bytesRead < maxBytes) {
					value <<= 7;
					value |= (value & 0x7f);
					byteRef = src.readByte();
				}
			} catch (err:Error) {}
			
			if (bytesRead < maxBytes) {
				value <<= 7;
				value |= byteRef;
			} else {
				value <<= 8;
				value |= byteRef;
				
				if ((value & 0x10000000) != 0) {
					value |= (~0xFFFFFFF);
				}
			}
			
			return value;
		}
		
		protected function writeVariableLengthInt(dst:ByteArray, value:uint):void
		{
			// can write variable length negative or positive integers (up to 4 bytes total)
			// otherwize use writeDouble
			if ((value & 0xffffff80) == 0) {
	            dst.writeByte(value & 0x7f);
	        } else  if ((value & 0xffffc000) == 0 ) {
	            dst.writeByte((value >>  7) | 0x80);
	            dst.writeByte(value & 0x7f);
	        } else if ((value & 0xffe00000) == 0) {
	            dst.writeByte((value >> 14) | 0x80);
	            dst.writeByte((value >>  7) | 0x80);
	            dst.writeByte(value & 0x7f);
	        } else {
		        dst.writeByte((value >> 22) | 0x80);
		        dst.writeByte((value >> 15) | 0x80);
		        dst.writeByte((value >>  8) | 0x80);
		        dst.writeByte(value & 0xff);
			}
		}
		
		protected function readString(src:ByteArray):String
		{
			return _readReferencedTypeObject(referencedStrings, src) as String;
		}
		
		protected function writeString(dst:ByteArray, value:String):void
		{
			var len:uint = (value != null)? value.length: 0;
			if (len == 0) {
				writeVariableLengthInt(dst, 0x01);
				return;
			} 
			
			if (!writeObjectReference(dst, value)) {
				dst.position--;
				dst.writeObject(value);
			}
		}
		
		protected function readBinaryString(src:ByteArray):String
		{
			// no reference tracking for binary strings
			var traitsInfo:uint = readVariableLengthInt(src);
			var len:uint = (traitsInfo >> 1);
			return src.readUTFBytes(len);
		}
		
		protected function writeBinaryString(dst:ByteArray, value:String):void
		{
			// no reference tracking for binary strings
			var len:uint = (value != null)? value.length: 0;
			var traitsInfo:uint = (len << 1) | 0x01;
			writeVariableLengthInt(dst, traitsInfo);
			dst.writeUTFBytes(value);
		}
		
		protected function readByteArray(src:ByteArray):ByteArray
		{
			return readObject(src) as ByteArray;
		}
		
		protected function writeByteArray(dst:ByteArray, value:ByteArray):void
		{
			if (!writeObjectReference(dst, value)) {
				dst.position--;
				dst.writeObject(value);
			}
		}
		
		protected function readArray(src:ByteArray):Array
		{
			return readObject(src) as Array;
		}
		
		protected function writeArray(dst:ByteArray, value:Array):void
		{
			if (!writeObjectReference(dst, value)) {
				dst.position--;
				dst.writeObject(value);
			}
		}
		
		protected function readDate(src:ByteArray):Date
		{
			return readObject(src) as Date;
		}
		
		protected function writeDate(dst:ByteArray, value:Date):void
		{
			if (!writeObjectReference(dst, value)) {
				dst.position--;
				dst.writeObject(value);
			}
		}
		
		protected function readXmlString(src:ByteArray):XML
		{
			return readObject(src) as XML;
		}
		
		protected function writeXmlString(dst:ByteArray, value:*):void
		{
			if (!writeObjectReference(dst, value)) {
				var xmlString:String;
				if (value is String) {
					xmlString = value as String;
				} else if (value is XML) {
					xmlString = (value as XML).toXMLString();
				} else if (value is XMLDocument) {
					xmlString = (value as XMLDocument).toString();
				} else {
					xmlString = value as String;
				}						
				writeBinaryString(dst, xmlString);
			}
		}
	}
}

